package com.wicketFlowTester;

import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.wicket.Component;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.Page;
import org.apache.wicket.ajax.markup.html.form.AjaxButton;
import org.apache.wicket.ajax.markup.html.form.AjaxCheckBox;
import org.apache.wicket.behavior.AbstractAjaxBehavior;
import org.apache.wicket.behavior.Behavior;
import org.apache.wicket.extensions.markup.html.tabs.TabbedPanel;
import org.apache.wicket.markup.html.form.Button;
import org.apache.wicket.markup.html.form.CheckBox;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.FormComponent;
import org.apache.wicket.protocol.http.WebApplication;
import org.apache.wicket.util.file.File;
import org.apache.wicket.util.visit.IVisit;
import org.apache.wicket.util.visit.IVisitor;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.wicketFlowTester.assertion.Attest;
import com.wicketFlowTester.framework.FormTesterFacade;
import com.wicketFlowTester.framework.WicketTesterFacade;
import com.wicketFlowTester.framework.WicketTesterFacadeImpl;

public class WicketFlowTester {

    private final WicketTesterFacade wicketTester;

    public static WicketFlowTester using(final WebApplication webApplication) {
        return new WicketFlowTester(webApplication);
    }
    
    public static WicketFlowTester getInstance() {
        return new WicketFlowTester();
    }

    private WicketFlowTester(final WebApplication webApplication) {
        wicketTester = WicketTesterFacadeImpl.getInstance(webApplication);
    }
    
    private WicketFlowTester() {
        wicketTester = WicketTesterFacadeImpl.getInstance();
    }

    public FlowTester startPage(final Page page) {
        wicketTester.startPage(page);
        return new FlowTester();
    }

    public class FlowTester {
        final private ListMultimap<Class<? extends Component>, Component> componentTypeMap = ArrayListMultimap.create();
        final private ListMultimap<String, Component> componentIdMap = ArrayListMultimap.create();
        final private Map<String, FormTesterFacade> formTesters = Maps.newHashMap();

        FlowTester() {
            wicketTester.getLastRenderedPage().visitChildren(new IVisitor<Component, Void>() {
                private Set<Component> visited = new HashSet<Component>();

                public void component(Component component, IVisit<Void> visit) {
                    if (!visited.contains(component)) {
                        visited.add(component);
                        if (component.isVisibilityAllowed() && component.isVisibleInHierarchy()) {
                            componentTypeMap.put(component.getClass(), component);
                            componentIdMap.put(component.getId(), component);
                        }
                    }
                }
            });
        }

        public FlowTester clickButton(final String buttonId) {
            final Button button = getComponent(buttonId);
            if (button instanceof AjaxButton) {
                wicketTester.executeAjaxEvent(button, "onclick");
            } else {
                if (button.getDefaultFormProcessing()) {
                    submitForm(button.getForm(), buttonId);
                } else {
                    button.onSubmit();
                }
            }
            return reloadPage();
        }

        public <T extends AbstractAjaxBehavior> FlowTester executeAjaxBehavior(final String componentId, final Class<T>... executeBehavior) {
            
            final List<Class<T>> executeBehaviors = Lists.newArrayList(executeBehavior);
            final Component component = getComponent(componentId);
            final List<? extends Behavior> behaviors = component.getBehaviors();
            for (Behavior behavior : behaviors) {
                if (behavior instanceof AbstractAjaxBehavior && isAssignableFrom(executeBehaviors, behavior)) {
                    wicketTester.executeBehavior((AbstractAjaxBehavior)behavior);
                }
            }
            return reloadPage();
        }
        
        public FlowTester setTab(final String componentId, final int tabIndex) {
            final TabbedPanel panel = getComponent(componentId);
            panel.setSelectedTab(tabIndex);
            return reloadPage();
        }
        
        private <S, T> boolean isAssignableFrom(final List<Class<T>> classList, final S object) {
            for (Class<T> clazz : classList) {
                if (clazz.isAssignableFrom(object.getClass())) {
                    return true;
                }
            }
            return false;
        }
        
        public FlowTester setValue(final String componentId, final String value) {
            return setValue(componentId, value, null);
        }

        public FlowTester setValue(final String componentId, final String value, final Comparator<Component> comparator, final int... indexes) {
            final List<Component> components = getComponents(componentId, comparator);
            new SelectiveIterator<Component>(components, indexes) {
                @Override
                public void process(final Component comp) {
                    if (comp instanceof FormComponent<?>) {
                        final FormTesterFacade tester = initFormTester(((FormComponent<?>)comp).getForm());
                        tester.setValue(getFormRelativeId((FormComponent<?>)comp), value);
                    } else {
                        Attest.fail("Not a Form-Component. Cannot set value: " + comp.getPath());
                    }
                }
            };
            return this;
        }
        
        public FlowTester setChecked(final String componentId, final boolean checked) {
            return setChecked(componentId, checked, null);
        }

        public <C extends CheckBox> FlowTester setChecked(final String componentId, final boolean checked,
                final Comparator<C> comparator, final int... indexes) {
            final List<C> components = getComponents(componentId, comparator);
            final Form<?> parent = components.get(0).getForm();
            final FormTesterFacade tester = initFormTester(parent);
            new SelectiveIterator<C>(components, indexes) {
                @Override
                public void process(final C chk) {
                    if (checked != chk.getModelObject().booleanValue()) {
                        tester.setValue(getFormRelativeId(chk), checked);
                        if (chk instanceof AjaxCheckBox) {
                            wicketTester.executeAjaxEvent(chk, "onclick");
                        } else {
                            chk.onSelectionChanged();
                        }
                    }
                }
            };
            if (!(components.get(0) instanceof AjaxCheckBox)) {
                (new FlowTester()).submitForm(parent, null);
            }
            return reloadPage();
        }

        private String getFormRelativeId(final FormComponent<?> component) {
            return component.getPath().substring(component.getForm().getPath().length() + 1);
        }
        
        public FlowTester verifyComponent(final String componentId, final boolean unique) {
            final List<Component> components = componentIdMap.get(componentId);
            if (unique) {
                 Attest.assertEquals("Component found should be unique for the given id.", 1, components.size());
            } else {
                Attest.assertTrue("Component(s) should exist for the given id.", components.size() > 0);
            }
            return this;
        }

        public FlowTester verifyCheckBoxStatus(final String componentId, final boolean checked) {
            return verifyCheckBoxStatus(componentId, checked, null);
        }

        public <C extends CheckBox> FlowTester verifyCheckBoxStatus(final String componentId, final boolean checked,
                final Comparator<C> comparator, final int... indexes) {
            final List<C> components = getComponents(componentId, comparator);
            new SelectiveIterator<C>(components, indexes) {
                @Override
                public void process(final C chk) {
                    Attest.assertEquals("Checkbox status should be as specified.", checked, chk.getModelObject()
                            .booleanValue());
                }
            };
            return this;
        }

        public FlowTester setFile(final String componentId, final File file, final String contentType) {
            final List<Component> components = componentIdMap.get(componentId);
            for (final Component comp : components) {
                if (comp instanceof FormComponent<?>) {
                    final FormTesterFacade tester = initFormTester(((FormComponent<?>)comp).getForm());
                    tester.setFile(comp.getId(), file, contentType);
                    wicketTester.getRequest().setUseMultiPartContentType(true);
                } else {
                    Attest.fail("Not a Form-Component. Cannot set value: " + comp.getPath());
                }
            }
            return this;
        }
        
        public FlowTester verifyValue(final String componentId, final String expected) {
            return verifyValue(componentId, expected, null);
        }

        public <C extends Component> FlowTester verifyValue(final String componentId, final String expected,
                final Comparator<C> comparator, final int... indexes) {
            final List<C> components = getComponents(componentId, comparator);
            new SelectiveIterator<C>(components, indexes) {
                @Override
                public void process(final C comp) {
                    Attest.assertEquals("Value on Component: " + componentId, expected,
                            comp.getDefaultModelObjectAsString());
                }
            };
            return this;
        }

        public <C extends Component> FlowTester matchesValue(final String componentId, final String pattern) {
            return matchesValue(componentId, pattern, null);
        }

        public <C extends Component> FlowTester matchesValue(final String componentId, final String pattern,
                final Comparator<C> comparator, final int... indexes) {
            final List<C> components = getComponents(componentId, comparator);
            new SelectiveIterator<C>(components, indexes) {
                @Override
                public void process(final C comp) {
                    final String actual = comp.getDefaultModelObjectAsString();
                    Attest.assertTrue(
                            String.format("Component Id:%s Value:%s should match %s.", componentId, actual, pattern),
                            actual.matches(pattern));
                }
            };
            return this;
        }

        public <T> FlowTester verifyEnabled(final boolean enabled, final String... componentIds) {
            for (String componentId : componentIds) {
                final List<Component> components = componentIdMap.get(componentId);
                for (Component component : components) {
                    Attest.assertEquals("isEnabled on Component: " + componentId + " - ", enabled, component.isEnabled());
                }
            }
            return this;
        }
        
        public <T> FlowTester verifyVisiblity(final boolean visible, final String... componentIds) {
            for (String componentId : componentIds) {
                final List<Component> components = componentIdMap.get(componentId);
                for (Component component : components) {
                    Attest.assertEquals("Visibility on Component: " + componentId + " - ", visible, component.isVisible());
                }
            }
            return this;
        }

        public <T> FlowTester verifyComponent(final ComponentVerifierByType componentVerifier) {
            componentVerifier.verify(componentTypeMap);
            return this;
        }
        
        public <T> FlowTester verifyComponent(final ComponentVerifierById componentVerifier) {
            componentVerifier.verify(componentIdMap);
            return this;
        }
        
        private void submitForm(final Form<?> form, final String buttonComponentId) {
            final FormTesterFacade tester = initFormTester(form);
            if (buttonComponentId == null) {
                tester.submit();
            } else {
                tester.submit(buttonComponentId);
            }
            formTesters.remove(form.getPath());
        }

        private FormTesterFacade initFormTester(final Form<?> form) {
            FormTesterFacade tester = formTesters.get(form.getPath());
            if (tester == null) {
                tester = wicketTester.newFormTester(getPath(form));
                formTesters.put(form.getPath(), tester);
            }
            return tester;
        }

        @SuppressWarnings("unchecked")
        private <T> T getComponent(final String componentId) {
            final List<T> components = (List<T>)componentIdMap.get(componentId);
            T component;
            if (components.size() != 1) {
                component = (T)wicketTester.getComponentFromLastRenderedPage(componentId);
            } else {
                component = components.get(0);
            }
            return component;
        }
        
        @SuppressWarnings("unchecked")
        private <T extends Component> List<T> getComponents(final String componentId, final Comparator<T> comparator) {
            final List<T> components = (List<T>)componentIdMap.get(componentId);
            if (comparator != null) {
                Collections.sort(components, comparator);
            }
            return components;
        }

        private String getPath(final Component component) {
            return component.getPath().substring(2);
        }
        
        private FlowTester reloadPage() {
            wicketTester.getLastRenderedPage().render();
            return new FlowTester();
        }

    }
    
    public static <T extends MarkupContainer> List<Component> getChild(final T component) {
        final List<Component> components = Lists.newArrayList();
        component.visitChildren(new IVisitor<Component, Void>() {
            public void component(Component object, IVisit<Void> visit) {
                components.add(component);
            }
        });
        return components;
    }
}
